[Swift]typealiasとclosureを使ってprotocolを使うほどではない場合にDependency Injectionを行えるように実装してみる
モジュールの設計を行う時にテスト容易性を担保するために具体的な実装ではなく抽象に依存するようにprotocolでインターフェースを定義して依存性を注入(Dependency Injection、DI)しやすいようにするパターンはSwift書かれたコードでよく見かけます。
自分もそのパターンを使ってinitializerやmethod injectionできるように作ることがあるのですが依存性がごく単純なものである場合にprotocolを使って抽象化するのがやりすぎに感じるときがあります。
protocolを使って抽象化を行うと依存性の実装を確認する時に型の名前から役割を想像することをできますがrequirementsとして実装を強制されるメソッドやプロパティはprotocolの定義元やconformしている型の定義元にジャンプして確認します。実質的に単一のメソッドのラッパーに過ぎない時にprotocolを使って抽象化を行うのは冗長に感じました。
そこでtypealias と closureを使うパターンを紹介します。もっとより良い方法があれば指摘いただけると嬉しいです。
typealiasについて
typealias keywordを使うことで既存の型の名前付きエイリアスをプログラムに導入できます。例えば頻繁にアプリケーションで使うことも多いCodableはDecodable & Encodable
の名前付きエイリアスです。これを使うことで存在する型の名前付きエイリアスをアクセス修飾子で定義したアクセスレベル内のどこでも使用できるようになります。protocol compositionなどを使用する時に命名が長くなってしまう時に使ったりします。他にも型名からは責務が想像できない時にも使ったりします。型パラを宣言できたりして結構便利に使えます。
ドキュメントは以下になります。
概観と具体例
typealiasを使ったDI自体についてはforumでも話が上がっていてよくあるパターンです。
typealiasを使ったDI自体はよくあるパターンで上記リンク先でもprotocol とtypealiasを使った型安全な抽象化をmatttさんが提案されていますし次の記事のようなprotocol compostionとtypealiasを使ったDIはアプリケーションコードでもライブラリのコードでもよく見かけます。
- Krzysztof Zabłocki - Using protocol compositon for dependency injection
- Dependency Injection in Swift with Protocols | Majid’s blog about Swift development
Dependency injectionには色々パターンがありますが今回はinitializer injectionのみを使います。その他のパターンについては以下が詳しいので必要な方は参照してください。
typealiasを使ってユーザー定義の構造体なりクラスなりのAliasを作ってDIを行おうとすると別の型に差し替えられた時に内部で使用しているメソッドなどのシグネチャが一致していればコンパイルが通ってしまいます。型安全にするためにprotocol を使うようにするのがベターだとforumでもコメントされています。
ただtypealiasはclosureにもaliasを与えることができます。
protocolが実質的に単一のメソッドのラッパーに過ぎないなら、メソッドをクロージャに変更してtypealiasでaliasを与えてそれに依存させることは、protocol程の抽象化が必要ないケースにおいてはそう悪くない選択肢に見えました。
そろそろコードと卑近な例を使いつつ説明を進めます。今回の例では ニュース一覧を返すTimelineGeneratorというクラスを定義します。TimelineGeneratorはprotocolで抽象化されている、記事のリストとお気に入りタグ一覧からおすすめの記事をArrayで返すFavoriteRecommenderという構造体に依存しています。記事を書きながらその場で考えた例なので多少わかりづらいかもしれませんが、そう感じた方はprotocolを通して抽象化された依存性を一つ持つオブジェクトを宣言していると理解してください。
import Foundation struct News { var title: String var description: String var referredList: [URL] } enum Tag { case tech, life, hobby, game, politic } protocol FavoritesRecommendable { func recommend(from newsList: [News], favoritedTags: [Tag]) -> [News] } struct FavoritesRecommender: FavoritesRecommendable { func recommend(from newsList: [News], favoritedTags: [Tag]) -> [News] { return // ... 何らかのアルゴリズムを実装しておすすめの記事一覧を返す } } class TimelineGenerator { private let newsList: [News] private let recommender: FavoritesRecommendable init(newsList: [News], recommender: FavoritesRecommender) { self.newsList = newsList self.recommender = recommender } }
protocolを存在型として扱う時のオーバーヘッドが気になるならTimelineGeneratorの定義は型パラメータを使うかもしれません。
class TimelineGenerator<Recommender: FavoritesRecommendable> { private let newsList: [News] private let recommender: Recommender init(newsList: [News], recommender: Recommender) { self.newsList = newsList self.recommender = recommender } }
すごく単純な例なのでぱっと見でprotocolを使ったデメリットは感じられませんが実際のアプリケーションコードだそれぞれのオブジェクトの実装がそれなりに長くなるので、あるオブジェクトの定義のスコープで閉じていない時はコードジャンプを使わざるを得ないケースがあります。
また、冒頭で説明したとおり依存性が実質的に単一のメソッドのラッパーです。それでも将来に備えてprotocolを使うケースもあります。しかしそうまでする必要がないケースもあります。そうでないケースで少し抽象度を下げて実装したい時にclosureが使えるかも知れません。そしてtypealiasを使うとその後protocolを使った書き方に差し替えることも難しいことではありません。
例です。先程の例からFavoritesRecommendableを削除します。FavoritesRecommendableのrequirementsのシグネチャを参考にtypealiasでクロージャの型を定義してAliasを宣言します。それを使ってイニシャライザなど必要な部分を書き換えます。@escaping属性を付与した上でデフォルト引数も使えばテスト時に実装を差し替えも容易です。
import Foundation struct News { var title: String var description: String var referredList: [URL] } enum Tag { case tech, life, hobby, game, politic } struct FavoritesRecommender { static func recommend(from newsList: [News], favoritedTags: [Tag]) -> [News] { return [] } } class TimelineGenerator { private let newsList: [News] typealias Favorites = (_ ewsList: [News], _ favoritedTags: [Tag]) -> [News] let favorites: Favorites init(newsList: [News], favorites: @escaping Favorites = FavoritesRecommender.recommend(from:favoritedTags:)) { self.newsList = newsList self.favorites = favorites } } TimelineGenerator(newsList: []) // 依存性を差し替えたい時は引数に値を与えたりtrailing closureを使って実装を提供できる。
protocolを使用するよりシンプルな実装で依存性を注入できる設計にできました。また、typealiasをTimelineGenerator内で宣言しているので他の定義場所にコードジャンプする必要もありません。要件が増えてクロージャでは済まなくなってもtypealiasの型は任意に書き換えられるのでコンパイルエラーで都度確認しながら修正してあらためてprotocolで抽象度を上げた実装に変更できます。typealiasを使わないパターンもありますがtypealiasは後から変更する時にすこし楽ができる点と名前付きエイリアスなので命名で責務を表現できるのがメリットだと感じました。
まとめ
モジュールの設計でどれぐらい抽象的な設計にするかは要件によると思います。Swiftはprotocol oriented programmingを志向していますが抽象化のあらゆるケースでprotocolが必要なわけではないと思っていて、より単純なケースではそれ相応に単純な実装で済ませたい気持ちがあります。
他のiOSエンジニアの方が今回のようなケースでどのような実装で済ませているのかが気になります。ライブラリのソースコードや案件で他の人の実装から学ぶことは多いですがこの記事の感想として代替案やよりベターな実装をご存じの方がおられたらコメントやTwitterにてご連絡いただけると嬉しいです。
最後まで読んでいただいてありがとうございました。